﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using NUnit.Framework.Constraints;

namespace Dgg.LearnYourTools.NUnit.C_Assertions.f_CustomConstraints
{
	[TestFixture]
	public class _031_ConstraintComposition
	{
		[Test]
		public void ConstrainedEnumerable_NumberOfItemsAndContraintsMismatch_Failure()
		{
			Assert.That(new[] { 1, 2 }, new ConstrainedEnumerable(Is.LessThan(3)));
		}

		[Test]
		public void ConstrainedEnumerable_ElementsSatisfyContraints_Success()
		{
			Assert.That(new[] { 1, 2 }, new ConstrainedEnumerable(Is.LessThan(3), new _030_GoingCustom.EvenConstraint()));
		}

		[Test]
		public void ConstrainedEnumerable_ElementsDoesNotSatisfyContraint_FailureIndicatingIndexAndFailingConstraint()
		{
			Assert.That(new[] { 1, 3 }, new ConstrainedEnumerable(Is.LessThan(0), new _030_GoingCustom.EvenConstraint()));
		}

		[Test]
		public void ConstrainedEnumerable_CanBeUsedWithCustomClasses_ButWatchHowThatClassIsRepresented()
		{
			Assert.That(
				new[]
				{
					new UglyCustomClass(1m),
					new UglyCustomClass(3m)
				},
				new ConstrainedEnumerable(
					Has.Property("Value").LessThan(2m),
					Has.Property("Value").EqualTo(4m)));
		}

		[Test]
		public void ConstrainedEnumerable_CanBeUsedWithCustomClasses_BetterFeedbackIfToStringIsImplemented()
		{
			Assert.That(
				new[]
				{
					new PrettyCustomClass(1m),
					new PrettyCustomClass(3m)
				},
				new ConstrainedEnumerable(
					Has.Property("Value").LessThan(2m),
					Has.Property("Value").EqualTo(4m)));
		}

		class UglyCustomClass
		{
			public UglyCustomClass(decimal value)
			{
				Value = value;
			}
			public decimal Value { get; private set; }
		}

		class PrettyCustomClass
		{
			public PrettyCustomClass(decimal value)
			{
				Value = value;
			}
			public decimal Value { get; private set; }

			public override string ToString()
			{
				return Value.ToString();
			}
		}
	}

	public class ConstrainedEnumerable : Constraint
	{
		public ConstrainedEnumerable(params Constraint[] constraints)
		{
			_constraints = constraints;
		}

		private readonly Constraint[] _constraints;
		public ConstrainedEnumerable(IEnumerable<Constraint> constraints)
		{
			_constraints = constraints.ToArray();
		}

		private Constraint _inner;
		private int _index;
		Array _collection;
		public override bool Matches(object current)
		{
			actual = current;

			// it is ok to iterate the collection as most of the times it will be a small controlled collection
			_collection = (current as IEnumerable).Cast<object>().ToArray();

			bool result = sameNumberOfElementsAndConstraints(_collection, _constraints.Length);
			if (result)
			{
				for (_index = 0; _index < _collection.Length && result; _index++)
				{
					_inner = new IndexedConstraint(_collection, _index, _constraints[_index]);
					result = _inner.Matches(_collection.GetValue(_index));
				}
			}
			return result;
		}

		private bool sameNumberOfElementsAndConstraints(Array current, int numberOfConstraints)
		{
			_inner = new MatchingLength(current, current.Length);
			return _inner.Matches(numberOfConstraints);
		}

		public override void WriteDescriptionTo(MessageWriter writer)
		{
			_inner.WriteDescriptionTo(writer);
		}

		public override void WriteMessageTo(MessageWriter writer)
		{
			_inner.WriteMessageTo(writer);
		}

		class MatchingLength : EqualConstraint
		{
			private readonly Array _items;

			public MatchingLength(Array items, object expected)
				: base(expected)
			{
				_items = items;
			}

			public override void WriteMessageTo(MessageWriter writer)
			{
				writer.WriteMessageLine(0, "{0} has a different number of items than constraints are provided.", getCollectionValue(writer));
				base.WriteMessageTo(writer);
			}

			private string getCollectionValue(MessageWriter writer)
			{
				writer.WriteActualValue(_items);
				var sb = writer.GetStringBuilder();
				string items = sb.ToString();
				sb.Remove(0, sb.Length);
				return items;
			}
		}

		class IndexedConstraint : Constraint
		{
			private readonly Array _items;
			private readonly int _index;
			private readonly Constraint _constraint;
			public IndexedConstraint(Array items, int index, IResolveConstraint constraint)
			{
				_items = items;
				_index = index;
				_constraint = constraint.Resolve();
			}

			public override bool Matches(object current)
			{
				actual = current;
				return _constraint.Matches(current);
			}

			public override void WriteDescriptionTo(MessageWriter writer)
			{
				_constraint.WriteDescriptionTo(writer);
			}

			public override void WriteActualValueTo(MessageWriter writer)
			{
				_constraint.WriteActualValueTo(writer);
			}

			public override void WriteMessageTo(MessageWriter writer)
			{
				string collectionValue = getCollectionValue(writer);
				writer.WriteMessageLine(0, "Element # {0} from {1} failed to satisfy constraint.", _index, collectionValue);
				base.WriteMessageTo(writer);
			}

			private string getCollectionValue(MessageWriter writer)
			{
				writer.WriteActualValue(_items);
				var sb = writer.GetStringBuilder();
				string items = sb.ToString();
				sb.Remove(0, sb.Length);
				return items;
			}
		}
	}
}
